package asteroids.model;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.antlr.v4.runtime.RecognitionException;

import asteroids.CollisionListener;
import asteroids.IFacade;
import asteroids.ModelException;
import asteroids.model.expressions.E;
import asteroids.model.programs.parsing.ProgramParser;
import asteroids.model.programs.Program;
import asteroids.model.programs.parsing.ProgramFactoryImpl;
import asteroids.model.statements.S;
import asteroids.model.types.T;

public class Facade implements IFacade<World, Ship, Asteroid, Bullet, Program> {

	/**
	   * Create a new world with the given <code>width</code> and
	   * <code>height</code>.
	   */
	  public World createWorld(double width, double height) {
		  try {
		  World world = new World(width, height);
		  return world;
		  }
		  catch (RuntimeException exp) {
			  throw new ModelException("something went wrong with creating a world");
		  }
	  }

	  /**
	   * Return the width of <code>world</code>.
	   */
	  public double getWorldWidth(World world){
		  return world.getWidth();
	  }

	  /**
	   * Return the height of <code>world</code>.
	   */
	  public double getWorldHeight(World world){
		  return world.getHeight();
	  }

	  /**
	   * Return all ships located within <code>world</code>.
	   */
	  public Set<Ship> getShips(World world){
		  return world.getShipList();
	  }

	  /**
	   * Return all asteroids located in <code>world</code>.
	   */
	  public Set<Asteroid> getAsteroids(World world){
		  return world.getAsteroidList();
	  }

	  /**
	   * Return all bullets located in <code>world</code>.
	   */
	  public Set<Bullet> getBullets(World world){
		  return world.getBulletList();
	  }

	  /**
	   * Add <code>ship</code> to <code>world</code>.
	   */
	  public void addShip(World world, Ship ship) throws ModelException {
		  try {
			  world.addFlyingObject(ship);		   
		  }
		  catch (RuntimeException exc) {
			  throw new ModelException("something went wrong when adding a ship");
		  }
	  }

	  /**
	   * Add <code>asteroid</code> to <code>world</code>.
	   */
	  public void addAsteroid(World world, Asteroid asteroid){
		  try {
		  world.addFlyingObject(asteroid);
		  }
		  catch (RuntimeException exp) {
			 throw new ModelException("something went wrong when adding an asteroid");
		  }
	  }

	  /**
	   * Remove <code>ship</code> from <code>world</code>.
	   */
	  public void removeShip(World world, Ship ship) {
		  try {
		  world.removeFlyingObject(ship);
		  }
		  catch (RuntimeException exp) {
			  throw new ModelException("could not remove the ship");
		  }
	  }

	  /**
	   * Remove <code>asteroid</code> from <code>world</code>.
	   */
	  public void removeAsteroid(World world, Asteroid asteroid){
		  try {
		  world.removeFlyingObject(asteroid);
		  }
		  catch (RuntimeException exp) {
			  throw new ModelException("could not remove the asteroid");
		  }
	  }

	  /**
	   * Advance <code>world</code> by <code>dt<code> seconds. 
	   * 
	   * To enable explosions within the UI, notify <code>collisionListener</code>
	   * whenever an entity collides with a boundary or another entity during this
	   * method. <code>collisionListener</code> may be null. If
	   * <code>collisionListener</code> is <code>null</code>, do not call its notify
	   * methods.
	   */
	  public void evolve(World world, double dt, CollisionListener collisionListener) {
		  try {
		  world.evolve(dt, collisionListener);
		  }
		  catch (RuntimeException exp) {
			 throw new ModelException("something went terribly wrong");
		  }
	  }

	  /**
	   * Create a new non-null ship with the given position, velocity, radius,
	   * direction and mass.
	   * 
	   * The thruster of the new ship is initially inactive. The ship is not located
	   * in a world.
	   */
	  public Ship createShip(double x, double y, double xVelocity, double yVelocity, double radius, double direction, double mass) {
		  Ship ship = new Ship(x,y,xVelocity,yVelocity, direction, radius, mass);
		  return ship;
	  }

	  /**
	   * Check whether <code>o</code> is a ship.
	   * 
	   * You can use the <code>instanceof</code> operator to implement this method.
	   */
	  public boolean isShip(Object o){
		  return (o instanceof Ship);
	  }

	  /**
	   * Return the x-coordinate of <code>ship</code>.
	   */
	  public double getShipX(Ship ship){
		  return ship.getPositionX();
	  }

	  /**
	   * Return the y-coordinate of <code>ship</code>.
	   */
	  public double getShipY(Ship ship){
		  return ship.getPositionY();
	  }

	  /**
	   * Return the velocity of <code>ship</code> along the X-axis.
	   */
	  public double getShipXVelocity(Ship ship){
		  return ship.getVelocityX();
	  }

	  /**
	   * Return the velocity of <code>ship</code> along the Y-axis.
	   */
	  public double getShipYVelocity(Ship ship){
		  return ship.getVelocityY();
	  }

	  /**
	   * Return the radius of <code>ship</code>.
	   */
	  public double getShipRadius(Ship ship){
		  return ship.getRadius();
	  }

	  /**
	   * Return the direction of <code>ship</code> (in radians).
	   */
	  public double getShipDirection(Ship ship){
		  return ship.getOrientation();
	  }

	  /**
	   * Return the mass of <code>ship</code>.
	   */
	  public double getShipMass(Ship ship){
		  return ship.getMass();
	  }

	  /**
	   * Return the world of <code>ship</code>.
	   */
	  public World getShipWorld(Ship ship){
		  return ship.getWorld();
	  }

	  /**
	   * Return whether <code>ship</code>'s thruster is active.
	   */
	  public boolean isShipThrusterActive(Ship ship){
		  return ship.getThruster();
	  }

	  /**
	   * Enables or disables <code>ship</code>'s thruster depending on the value of
	   * the parameter <code>active</code>.
	   */
	  public void setThrusterActive(Ship ship, boolean active){
		  ship.setThruster(active);
	  }

	  /**
	   * Update the direction of <code>ship</code> by adding <code>angle</code> (in
	   * radians) to its current direction. <code>angle</code> may be negative.
	   */
	  public void turn(Ship ship, double angle){
		 try {
		  ship.turn(angle);
		 }
		 catch (RuntimeException exp) {
			 throw new ModelException("could not turn the ship");
		 }
	  }

	  /**
	   * <code>ship</code> fires a bullet.
	   */
	  public void fireBullet(Ship ship){
		  try {
		  ship.fire();
		  }
		  catch (RuntimeException exp) {
			  throw new ModelException("unaible to fire a bullet");
		  }
	  }

	  /**
	   * Create a new non-null asteroid with the given position, velocity and
	   * radius.
	   * 
	   * The asteroid is not located in a world.
	   */
	  public Asteroid createAsteroid(double x, double y, double xVelocity, double yVelocity, double radius){
		  try {
		  Asteroid asteroid = new Asteroid(x, y, xVelocity, yVelocity, radius);
		  return asteroid;
		  }
		  catch (RuntimeException exp) {
			  throw new ModelException("could not create the asteroid");
		  }
	  }

	  /**
	   * Create a new non-null asteroid with the given position, velocity and
	   * radius.
	   * 
	   * The asteroid is not located in a world.
	   * 
	   * Use numbers generated by <code>random</code> to determine the direction of
	   * the children (if any) when this asteroid dies.
	   */
	  public Asteroid createAsteroid(double x, double y, double xVelocity, double yVelocity, double radius, Random random){
		  try {
		  return new Asteroid(x,y, xVelocity, yVelocity, radius, random);
		  }
		  catch (RuntimeException exp) {
			  throw new ModelException("could not create the modelException");
		  }
	  }

	  /**
	   * Check whether <code>o</code> is an asteroid.
	   * 
	   * You can use the <code>instanceof</code> operator to implement this method.
	   */
	  public boolean isAsteroid(Object o){
		  return (o instanceof Asteroid);
	  }

	  /**
	   * Return the x-coordinate of <code>asteroid</code>.
	   */
	  public double getAsteroidX(Asteroid asteroid){
		  return asteroid.getPositionX();
	  }

	  /**
	   * Return the y-coordinate of <code>asteroid</code>.
	   */
	  public double getAsteroidY(Asteroid asteroid){
		  return asteroid.getPositionY();
	  }

	  /**
	   * Return the velocity of <code>asteroid</code> along the X-axis.
	   */
	  public double getAsteroidXVelocity(Asteroid asteroid){
		  return asteroid.getVelocityX();
	  }

	  /**
	   * Return the velocity of <code>asteroid</code> along the Y-axis.
	   */
	  public double getAsteroidYVelocity(Asteroid asteroid){
		  return asteroid.getVelocityY();
	  }

	  /**
	   * Return the radius of <code>asteroid</code>.
	   */
	  public double getAsteroidRadius(Asteroid asteroid){
		  return asteroid.getRadius();
	  }

	  /**
	   * Return the mass of <code>asteroid</code>.
	   */
	  public double getAsteroidMass(Asteroid asteroid){
		  return asteroid.getMass();
	  }

	  /**
	   * Return the world of <code>asteroid</code>.
	   */
	  public World getAsteroidWorld(Asteroid asteroid){
		  return asteroid.getWorld();
	  }

	  /**
	   * Check whether <code>o</code> is a bullet.
	   * 
	   * You can use the <code>instanceof</code> operator to implement this method.
	   */
	  public boolean isBullets(Object o){
		  return (o instanceof Bullet);
	  }

	  /**
	   * Return the x-coordinate of <code>bullet</code>.
	   */
	  public double getBulletX(Bullet bullet){
		  return bullet.getPositionX();
	  }

	  /**
	   * Return the y-coordinate of <code>bullet</code>.
	   */
	  public double getBulletY(Bullet bullet){
		  return bullet.getPositionY();
	  }

	  /**
	   * Return the velocity of <code>bullet</code> along the X-axis.
	   */
	  public double getBulletXVelocity(Bullet bullet){
		  return bullet.getVelocityX();
	  }

	  /**
	   * Return the velocity of <code>bullet</code> along the Y-axis.
	   */
	  public double getBulletYVelocity(Bullet bullet){
		  return bullet.getVelocityY();
	  }

	  /**
	   * Return the radius of <code>bullet</code>.
	   */
	  public double getBulletRadius(Bullet bullet){
		  return bullet.getRadius();
	  }

	  /**
	   * Return the mass of <code>bullet</code>.
	   */
	  public double getBulletMass(Bullet bullet){
		  return bullet.getMass();
	  }

	  /**
	   * Return the world of <code>bullet</code>.
	   */
	  public World getBulletWorld(Bullet bullet){
		  return bullet.getWorld();
	  }

	  /**
	   * Return the source of <code>bullet</code>.
	   */
	  public Ship getBulletSource(Bullet bullet){
		  return bullet.getSource();
	  }

	@Override
	public asteroids.IFacade.ParseOutcome<Program> parseProgram(String text) {
		ProgramFactoryImpl factory = new ProgramFactoryImpl();
	    ProgramParser<E, S, T> parser = new ProgramParser<>(factory);
	    try {
	        parser.parse(text);
	        List<String> errors = parser.getErrors();
	        if(! errors.isEmpty()) {
	          return ParseOutcome.failure(errors.get(0));
	        } else {
	          return ParseOutcome.success(new Program(parser.getGlobals(), parser.getStatement())); 
	        }
	    } catch(RecognitionException e) {
	      return ParseOutcome.failure(e.getMessage());
	    }
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> loadProgramFromStream(
			InputStream stream) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
		String text = "";
		for (String line = reader.readLine(); line != null; line = reader.readLine()) {
			text = text + "\n" + line;
		}
		ProgramFactoryImpl factory = new ProgramFactoryImpl();
	    ProgramParser<E, S, T> parser = new ProgramParser<>(factory);
	    try {
	        parser.parse(text);
	        
	        List<String> errors = parser.getErrors();
	        if(! errors.isEmpty()) {
	          return ParseOutcome.failure(errors.get(0));
	        } else {
	          return ParseOutcome.success(new Program(parser.getGlobals(), parser.getStatement())); 
	        }
	    } catch(RecognitionException e) {
	      return ParseOutcome.failure(e.getMessage());
	    }
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> loadProgramFromUrl(URL url)
			throws IOException {
		InputStream inputStream = url.openStream();
		BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		String text = "";
		for (String line = reader.readLine(); line != null; line = reader.readLine()) {
			text = text + "\n" + line;
		}
		ProgramFactoryImpl factory = new ProgramFactoryImpl();
	    ProgramParser<E, S, T> parser = new ProgramParser<>(factory);
	    try {
	        parser.parse(text);
	        //
	        List<String> errors = parser.getErrors();
	        if(! errors.isEmpty()) {
	          return ParseOutcome.failure(errors.get(0));
	        } else {
	          return ParseOutcome.success(new Program(parser.getGlobals(), parser.getStatement())); 
	        }
	    } catch(RecognitionException e) {
	      return ParseOutcome.failure(e.getMessage());
	    }
	}
	

	@Override
	public boolean isTypeCheckingSupported() {
		return true;
	}

	@Override
	public asteroids.IFacade.TypeCheckOutcome typeCheckProgram(Program program) {
		if(program.typeCheck(program.getGlobals()))
			return TypeCheckOutcome.success();
		else
			return asteroids.IFacade.TypeCheckOutcome.failure("failure");
		
	}

	@Override
	public void setShipProgram(Ship ship, Program program) {
		ship.setProgram(program);
		program.setShip(ship);		
	}

}
